use core::ptr::{read_volatile, write_volatile};

#[cfg(feature = "axstd")]
use axplat::mem::{pa, phys_to_virt};
const OPEN_FLAGS: c_int = 0o2;

const DINO_GROUND_Y: usize = 10;
const DINO_X: usize = 5;
const DINO_INITIAL_OBSTACLE_X: usize = 40;
const DINO_WIDTH: usize = 50;
const DINO_GAME_SPEED: usize = 3;
const DINO_FRAMES_PER_RENDER: usize = 2;
const DINO_TICK_MILLIS: u64 = 60;

pub fn run() {
    let mut args = collect_args().into_iter();
    let program = args.next().unwrap_or_else(|| "sbox".into());
    let Some(command) = args.next() else {
        print_usage();
        return;
    };

    let rest: Vec<String> = args.collect();
    match command.as_str() {
        "gpio" => {
            if let Err(err) = handle_gpio(rest) {
                println!("{program}: gpio: {err}");
            }
        }
        "reboot" => {
            if let Err(err) = handle_reboot(rest) {
                println!("{program}: reboot: {err}");
            }
        }
        "led_blink" => {
            if let Err(err) = handle_led_blink(rest) {
                println!("{program}: led_blink: {err}");
            }
        }
        "led_breath" => {
            if let Err(err) = handle_led_breath(rest) {
                println!("{program}: led_breath: {err}");
            }
        }
        "dino" => {
            if let Err(err) = handle_dino(rest) {
                println!("{program}: dino: {err}");
            }
        }
        "detect_button" => {
            if let Err(err) = handle_detect_button(rest) {
                println!("{program}: detect_button: {err}");
            }
        }
        "help" => print_usage(),
        other => {
            println!("{program}: unknown command '{other}'");
            print_usage();
        }
    }
}

struct DeviceFile {
    fd: c_int,
}

impl DeviceFile {
    fn open(path: &str) -> Result<Self, String> {
        let cstr = into_c_string(path)?;
        let fd = unsafe { open(cstr.as_ptr(), OPEN_FLAGS, 0) };
        if fd < 0 {
            return Err(format!("failed to open {path} (errno {})", errno()));
        }
        Ok(Self { fd })
    }

    fn ioctl<T>(&self, cmd: u32, data: &mut T) -> Result<(), String> {
        let ret = unsafe { ioctl(self.fd, cmd as c_ulong, (data as *mut T).cast::<c_void>()) };
        if ret < 0 {
            return Err(format!("ioctl 0x{cmd:08x} failed (errno {})", errno()));
        }
        Ok(())
    }
}

impl Drop for DeviceFile {
    fn drop(&mut self) {
        if self.fd >= 0 {
            unsafe {
                close(self.fd);
            }
        }
    }
}

struct GpioClient {
    dev: DeviceFile,
}

impl GpioClient {
    fn new() -> Result<Self, String> {
        Ok(Self {
            dev: DeviceFile::open(GPIO_DEVICE_PATH)?,
        })
    }

    fn set(&self, bank: usize, pin: u8, level: gpio_proto::Level) -> Result<(), String> {
        let mut req = gpio_proto::Request::new(bank as u32, pin as u32, level);
        self.dev.ioctl(gpio_proto::IOCTL_SET, &mut req)
    }

    fn get(&self, bank: usize, pin: u8) -> Result<gpio_proto::Level, String> {
        let mut req = gpio_proto::Request::new(bank as u32, pin as u32, gpio_proto::Level::Low);
        self.dev.ioctl(gpio_proto::IOCTL_GET, &mut req)?;
        Ok(req.level)
    }

    fn toggle(&self, bank: usize, pin: u8) -> Result<(), String> {
        let mut req = gpio_proto::Request::new(bank as u32, pin as u32, gpio_proto::Level::Low);
        self.dev.ioctl(gpio_proto::IOCTL_TOGGLE, &mut req)
    }
}

fn collect_args() -> Vec<String> {
    #[cfg(not(target_os = "none"))]
    {
        env::args().collect()
    }
    #[cfg(target_os = "none")]
    {
        match fs::read("/proc/self/cmdline") {
            Ok(data) => parse_cmdline(&data),
            Err(err) => {
                println!("sbox: failed to read /proc/self/cmdline: {err}");
                Vec::new()
            }
        }
    }
}

#[cfg(target_os = "none")]
fn parse_cmdline(raw: &[u8]) -> Vec<String> {
    if raw.is_empty() {
        return Vec::new();
    }
    let mut parts = Vec::new();
    let mut start = 0usize;
    for (idx, byte) in raw.iter().enumerate() {
        if *byte == 0 {
            if idx > start {
                if let Ok(part) = str::from_utf8(&raw[start..idx]) {
                    parts.push(part.to_owned());
                }
            }
            start = idx + 1;
        }
    }
    if start < raw.len() {
        if let Ok(part) = str::from_utf8(&raw[start..]) {
            parts.push(part.to_owned());
        }
    }
    parts
}

fn into_c_string(path: &str) -> Result<Vec<c_char>, String> {
    if path.as_bytes().contains(&0) {
        return Err("device path contains interior nul byte".to_string());
    }
    let mut bytes = path.as_bytes().to_vec();
    bytes.push(0);
    Ok(bytes.into_iter().map(|b| b as c_char).collect())
}

fn errno() -> i32 {
    #[cfg(any(target_env = "gnu", target_env = "musl"))]
    unsafe {
        *__errno_location()
    }
    #[cfg(not(any(target_env = "gnu", target_env = "musl")))]
    {
        0
    }
}

unsafe extern "C" {
    fn open(path: *const c_char, flags: c_int, ...) -> c_int;
    fn close(fd: c_int) -> c_int;
    fn ioctl(fd: c_int, cmd: c_ulong, arg: *mut c_void) -> c_int;
    #[cfg(any(target_env = "gnu", target_env = "musl"))]
    fn __errno_location() -> *mut c_int;
}

struct TimerClient {
    dev: DeviceFile,
}

impl TimerClient {
    fn new() -> Result<Self, String> {
        Ok(Self {
            dev: DeviceFile::open(TIMER_DEVICE_PATH)?,
        })
    }

    fn init(&self, channel: u32) -> Result<(), String> {
        let mut req = timer_proto::ChannelRequest::new(channel);
        self.dev.ioctl(timer_proto::IOCTL_INIT, &mut req)
    }

    fn disable(&self, channel: u32) -> Result<(), String> {
        let mut req = timer_proto::ChannelRequest::new(channel);
        self.dev.ioctl(timer_proto::IOCTL_DISABLE, &mut req)
    }

    fn wait_ticks(&self, channel: u32, ticks: u64) -> Result<u64, String> {
        let mut req = timer_proto::WaitRequest::new(channel, ticks);
        self.dev.ioctl(timer_proto::IOCTL_WAIT_TICKS, &mut req)?;
        Ok(req.observed)
    }

    fn wait_seconds(&self, channel: u32, seconds: u64) -> Result<(), String> {
        let ticks = seconds
            .checked_mul(TIMER_FREQ_HZ)
            .ok_or_else(|| "timer ticks overflow".to_string())?;
        self.wait_ticks(channel, ticks).map(|_| ())
    }

    fn wait_micros(&self, channel: u32, micros: u64) -> Result<(), String> {
        let mut req = timer_proto::MicrosRequest::new(channel, micros);
        self.dev.ioctl(timer_proto::IOCTL_WAIT_MICROS, &mut req)?;
        Ok(())
    }
}

struct PmuClient {
    dev: DeviceFile,
}

impl PmuClient {
    fn new() -> Result<Self, String> {
        Ok(Self {
            dev: DeviceFile::open(PMU_DEVICE_PATH)?,
        })
    }

    fn reboot(&self, level: pmu_proto::RebootLevel) -> Result<(), String> {
        let mut req = pmu_proto::RebootRequest::new(level);
        self.dev.ioctl(pmu_proto::IOCTL_REBOOT, &mut req)
    }
}

fn handle_gpio(args: Vec<String>) -> Result<(), String> {
    if args.is_empty() || matches!(args[0].as_str(), "-h" | "--help") {
        print_gpio_usage();
        return Ok(());
    }

    let subcmd = &args[0];
    let command = match gpio_cmd::parse_command(subcmd, args[1..].iter().map(|s| s.as_str())) {
        Ok(command) => command,
        Err(err) => {
            println!("gpio: {err}");
            print_gpio_usage();
            return Ok(());
        }
    };

    let gpio = GpioClient::new()?;

    match command {
        GpioCommand::Set { bank, pin, level } => {
            gpio.set(bank, pin, to_proto_level(level))?;
            println!(
                "gpio: bank {bank} pin {pin} set to {}",
                gpio_cmd::display_level(level)
            );
        }
        GpioCommand::Get { bank, pin } => {
            let level = gpio.get(bank, pin)?;
            println!(
                "gpio: bank {bank} pin {pin} is {}",
                display_proto_level(level)
            );
        }
        GpioCommand::Toggle { bank, pin } => {
            gpio.toggle(bank, pin)?;
            println!("gpio: bank {bank} pin {pin} toggled");
        }
        GpioCommand::Help => print_gpio_usage(),
    }

    Ok(())
}

fn handle_reboot(args: Vec<String>) -> Result<(), String> {
    if matches!(args.first().map(|s| s.as_str()), Some("-h" | "--help")) {
        print_reboot_usage();
        return Ok(());
    }

    if !args.is_empty() {
        for arg in args {
            println!("reboot: unexpected argument '{arg}'");
        }
        print_reboot_usage();
        return Ok(());
    }

    println!();
    println!("╔══════════════════════════════════════════════════════╗");
    println!("║          CRU System Reboot (First Level Reset)        ║");
    println!("╚══════════════════════════════════════════════════════╝");
    println!("This will reset almost all logic except hardware-reset-only registers.");
    println!();
    println!("System will reboot in approximately 3 seconds.");

    countdown_with_warning();

    let pmu = PmuClient::new()?;
    println!("Triggering system reboot now...");
    pmu.reboot(pmu_proto::RebootLevel::First)?;
    println!("reboot: request issued, waiting for hardware reset...");
    loop {
        core::hint::spin_loop();
    }
}

fn handle_led_blink(args: Vec<String>) -> Result<(), String> {
    if matches!(args.first().map(|s| s.as_str()), Some("-h" | "--help")) {
        print_led_blink_usage();
        return Ok(());
    }

    if !args.is_empty() {
        for arg in args {
            println!("led_blink: unexpected argument '{arg}'");
        }
        print_led_blink_usage();
        return Ok(());
    }

    match led_blink_sequence() {
        Ok(()) => println!("led_blink: completed"),
        Err(err) => println!("led_blink: {err}"),
    }
    Ok(())
}

fn handle_led_breath(args: Vec<String>) -> Result<(), String> {
    if matches!(args.first().map(|s| s.as_str()), Some("-h" | "--help")) {
        print_led_breath_usage();
        return Ok(());
    }

    if !args.is_empty() {
        for arg in args {
            println!("led_breath: unexpected argument '{arg}'");
        }
        print_led_breath_usage();
        return Ok(());
    }

    match led_breath_sequence() {
        Ok(()) => println!("led_breath: completed"),
        Err(err) => println!("led_breath: {err}"),
    }
    Ok(())
}

fn handle_dino(args: Vec<String>) -> Result<(), String> {
    if matches!(args.first().map(|s| s.as_str()), Some("-h" | "--help")) {
        print_dino_usage();
        return Ok(());
    }

    if !args.is_empty() {
        for arg in args {
            println!("dino: unexpected argument '{arg}'");
        }
        print_dino_usage();
        return Ok(());
    }

    run_dino_game()
}

fn handle_detect_button(args: Vec<String>) -> Result<(), String> {
    if matches!(args.first().map(|s| s.as_str()), Some("-h" | "--help")) {
        print_detect_button_usage();
        return Ok(());
    }

    if !args.is_empty() {
        for arg in args {
            println!("detect_button: unexpected argument '{arg}'");
        }
        print_detect_button_usage();
        return Ok(());
    }

    match detect_button_sequence() {
        Ok(()) => println!("detect_button: completed"),
        Err(err) => println!("detect_button: {err}"),
    }
    Ok(())
}

fn led_blink_sequence() -> Result<(), String> {
    println!("Blinking blue LED with 1s intervals (3 cycles)...");

    let gpio = GpioClient::new()?;
    let timer = TimerClient::new()?;

    timer.init(TIMER_CHANNEL_BLINK)?;
    println!("Timer channel {TIMER_CHANNEL_BLINK} initialized.");

    for index in 0..3 {
        let display_index = index + 1;
        gpio.set(GPIO3_BANK, BLUE_LED_PIN, gpio_proto::Level::High)?;
        println!("Blink #{display_index}: LED ON");
        timer.wait_seconds(TIMER_CHANNEL_BLINK, 1)?;

        gpio.set(GPIO3_BANK, BLUE_LED_PIN, gpio_proto::Level::Low)?;
        println!("Blink #{display_index}: LED OFF");
        timer.wait_seconds(TIMER_CHANNEL_BLINK, 1)?;
    }

    gpio.set(GPIO3_BANK, BLUE_LED_PIN, gpio_proto::Level::Low)?;
    timer.disable(TIMER_CHANNEL_BLINK)?;
    Ok(())
}

fn led_breath_sequence() -> Result<(), String> {
    println!("Starting green LED breathing effect (software PWM, {SOFT_PWM_FREQ_HZ} Hz)...");

    let gpio = GpioClient::new()?;
    let timer = TimerClient::new()?;
    timer.init(TIMER_CHANNEL_BREATH)?;
    println!("Timer channel {TIMER_CHANNEL_BREATH} initialized at {TIMER_FREQ_HZ} Hz base.");

    let min_duty = 5u64;
    let max_duty = 100u64;
    let steps = 50u64;
    let periods_per_step = 20;
    let cycles = 3;

    for cycle in 1..=cycles {
        println!("Breathing cycle {cycle}/{cycles} (brightening)");
        for step in 0..=steps {
            let duty = min_duty + ((max_duty - min_duty) * step) / steps;
            for _ in 0..periods_per_step {
                soft_pwm_period(&gpio, &timer, TIMER_CHANNEL_BREATH, duty)?;
            }
        }

        println!("Breathing cycle {cycle}/{cycles} (dimming)");
        for step in (0..=steps).rev() {
            let duty = min_duty + ((max_duty - min_duty) * step) / steps;
            for _ in 0..periods_per_step {
                soft_pwm_period(&gpio, &timer, TIMER_CHANNEL_BREATH, duty)?;
            }
        }
    }

    gpio.set(GPIO3_BANK, GREEN_LED_PIN, gpio_proto::Level::Low)?;
    timer.disable(TIMER_CHANNEL_BREATH)?;
    println!("Breathing effect complete, green LED off.");
    Ok(())
}

fn detect_button_sequence() -> Result<(), String> {
    println!("Waiting for button press on GPIO1_A4 (header pin 11).");
    println!("Connect the other leg of the switch to GND (header pin 9).");

    // TODO: not working
    enable_button_pullup()?;

    let gpio = GpioClient::new()?;
    let mut last_level = gpio.get(BUTTON_BANK, BUTTON_PIN)?;
    println!(
        "Initial level observed: {}",
        display_proto_level(last_level)
    );

    loop {
        let level = gpio.get(BUTTON_BANK, BUTTON_PIN)?;
        if level == gpio_proto::Level::Low && last_level == gpio_proto::Level::High {
            println!("Button press detected on GPIO1_A4!");
            break;
        }

        last_level = level;
        spin_delay(BUTTON_POLL_SPIN_LOOPS);
    }

    Ok(())
}

fn enable_button_pullup() -> Result<(), String> {
    println!("Configuring internal pull-up for GPIO1_A4...");
    enable_button_pullup_impl()
}

fn pullup_encoding() -> (u32, u32, u32, u32) {
    let pin_index = (BUTTON_PIN as u32) % PULL_PINS_PER_REG;
    let bit_shift = pin_index * PULL_BITS_PER_PIN;
    let mask = ((1u32 << PULL_BITS_PER_PIN) - 1) << (bit_shift + 16);
    let value = PULL_UP_CODE << bit_shift;
    let data = mask | value;
    (mask, value, data, bit_shift)
}

#[cfg(feature = "axstd")]
fn enable_button_pullup_impl() -> Result<(), String> {
    let pull_reg_phys = VCCIO1_4_IOC_BASE + GPIO1A_PULL_OFFSET;
    let pull_reg = phys_to_virt(pa!(pull_reg_phys)).as_usize();
    let (mask, value, data, bit_shift) = pullup_encoding();

    let before = unsafe { read_volatile(pull_reg as *const u32) };
    unsafe { write_volatile(pull_reg as *mut u32, data) };
    let after = unsafe { read_volatile(pull_reg as *const u32) };

    println!(
        "Pull register 0x{pull_reg_phys:08X}: before=0x{before:08X}, after=0x{after:08X}, mask=0x{mask:08X}, value=0x{value:08X} (bits {}..{}).",
        bit_shift + 1,
        bit_shift
    );
    Ok(())
}

#[cfg(not(feature = "axstd"))]
fn enable_button_pullup_impl() -> Result<(), String> {
    let pull_reg_phys = VCCIO1_4_IOC_BASE + GPIO1A_PULL_OFFSET;
    let (mask, value, data, bit_shift) = pullup_encoding();
    let page_base = pull_reg_phys & !(VCCIO_MMAP_LENGTH - 1);
    let page_offset = pull_reg_phys - page_base;

    let file = match OpenOptions::new()
        .read(true)
        .write(true)
        .open(DEV_MEM_PATH)
    {
        Ok(file) => file,
        Err(err) if err.kind() == ErrorKind::NotFound => {
            println!(
                "Warning: {DEV_MEM_PATH} not found; skipping pull-up configuration (GPIO may rely on external bias)."
            );
            return Ok(());
        }
        Err(err) if err.kind() == ErrorKind::PermissionDenied => {
            println!(
                "Warning: insufficient permissions to open {DEV_MEM_PATH}; skipping pull-up configuration (run as root to enable)."
            );
            return Ok(());
        }
        Err(err) => return Err(format!("failed to open {DEV_MEM_PATH}: {err}")),
    };
    let fd = file.as_raw_fd();

    let mapping = unsafe {
        libc::mmap(
            core::ptr::null_mut(),
            VCCIO_MMAP_LENGTH,
            libc::PROT_READ | libc::PROT_WRITE,
            libc::MAP_SHARED,
            fd,
            page_base as libc::off_t,
        )
    };

    if mapping == libc::MAP_FAILED {
        drop(file);
        let err = io::Error::last_os_error();
        return Err(format!(
            "mmap {DEV_MEM_PATH} at phys 0x{page_base:08X} failed: {err}"
        ));
    }

    drop(file);

    let reg_ptr = unsafe { (mapping as *mut u8).add(page_offset) as *mut u32 };
    let before = unsafe { read_volatile(reg_ptr) };
    unsafe { write_volatile(reg_ptr, data) };
    let after = unsafe { read_volatile(reg_ptr) };

    println!(
        "Pull register 0x{pull_reg_phys:08X}: before=0x{before:08X}, after=0x{after:08X}, mask=0x{mask:08X}, value=0x{value:08X} (bits {}..{}).",
        bit_shift + 1,
        bit_shift
    );

    let unmap_result = unsafe { libc::munmap(mapping, VCCIO_MMAP_LENGTH) };
    if unmap_result != 0 {
        let err = io::Error::last_os_error();
        println!(
            "Warning: munmap of phys 0x{page_base:08X} failed: {err} (continuing)."
        );
    }

    Ok(())
}

fn run_dino_game() -> Result<(), String> {
    println!("Chrome Dino Game");
    println!("Instructions:");
    println!("  - Press SPACE to jump over obstacles");
    println!("  - Press 'q' to quit");
    println!("  - The game runs automatically");
    println!("\nPress ENTER to start...");

    wait_for_dino_start()?;

    let mut input = DinoInput::new();
    let mut game = DinoGame::new();
    game.run(&mut input)
}

fn wait_for_dino_start() -> Result<(), String> {
    let mut stdin = io::stdin();
    let mut buf = [0u8; 1];
    stdin
        .read(&mut buf)
        .map(|_| ())
        .map_err(|err| format!("failed to read stdin: {err}"))
}

#[cfg(feature = "axstd")]
struct DinoInput {
    stdin: io::Stdin,
}

#[cfg(feature = "axstd")]
impl DinoInput {
    fn new() -> Self {
        Self { stdin: io::stdin() }
    }

    fn poll(&mut self) -> Option<u8> {
        let mut buf = [0u8; 1];
        let mut lock = self.stdin.lock();
        match lock.read(&mut buf) {
            Ok(0) => None,
            Ok(_) => Some(buf[0]),
            Err(_) => None,
        }
    }

    fn wait_for_quit(&mut self) {
        let mut buf = [0u8; 1];
        loop {
            match self.stdin.read(&mut buf) {
                Ok(0) => thread::yield_now(),
                Ok(_) if matches!(buf[0], b'q' | b'Q') => break,
                Ok(_) => {}
                Err(_) => thread::yield_now(),
            }
        }
    }
}

#[cfg(not(feature = "axstd"))]
struct DinoInput {
    rx: mpsc::Receiver<u8>,
}

#[cfg(not(feature = "axstd"))]
impl DinoInput {
    fn new() -> Self {
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || {
            let mut stdin = io::stdin();
            let mut buf = [0u8; 1];
            while stdin.read(&mut buf).ok() == Some(1) {
                if tx.send(buf[0]).is_err() {
                    break;
                }
            }
        });
        Self { rx }
    }

    fn poll(&mut self) -> Option<u8> {
        match self.rx.try_recv() {
            Ok(byte) => Some(byte),
            Err(TryRecvError::Empty) => None,
            Err(TryRecvError::Disconnected) => None,
        }
    }

    fn wait_for_quit(&mut self) {
        while let Ok(byte) = self.rx.recv() {
            if matches!(byte, b'q' | b'Q') {
                break;
            }
        }
    }
}

struct DinoGame {
    dino_y: usize,
    dino_velocity: i32,
    obstacle_x: usize,
    score: u32,
    game_over: bool,
    is_jumping: bool,
    frame_count: usize,
}

impl DinoGame {
    fn new() -> Self {
        Self {
            dino_y: DINO_GROUND_Y,
            dino_velocity: 0,
            obstacle_x: DINO_INITIAL_OBSTACLE_X,
            score: 0,
            game_over: false,
            is_jumping: false,
            frame_count: 0,
        }
    }

    fn jump(&mut self) {
        if self.dino_y == DINO_GROUND_Y && !self.game_over {
            self.dino_velocity = -3;
            self.is_jumping = true;
        }
    }

    fn update(&mut self) {
        if self.dino_y < DINO_GROUND_Y || self.dino_velocity < 0 {
            self.dino_velocity += 1;
            let new_y = (self.dino_y as i32) + self.dino_velocity;
            self.dino_y = if new_y > DINO_GROUND_Y as i32 {
                self.is_jumping = false;
                DINO_GROUND_Y
            } else {
                new_y.max(0) as usize
            };
        }

        if self.obstacle_x > 0 {
            self.obstacle_x -= 1;
        } else {
            self.obstacle_x = DINO_WIDTH;
            self.score = self.score.saturating_add(10);
        }

        if self.obstacle_x >= DINO_X.saturating_sub(1)
            && self.obstacle_x <= DINO_X.saturating_add(2)
        {
            if self.dino_y >= DINO_GROUND_Y.saturating_sub(1) {
                self.game_over = true;
            }
        }
    }

    fn render(&self) -> Result<(), String> {
        print!("\x1B[2J\x1B[H");

        println!(
            "Score: {}  (Press SPACE to jump, 'q' to quit)",
            self.score
        );
        println!();

        for y in 0..=DINO_GROUND_Y + 2 {
            for x in 0..DINO_WIDTH {
                if y == DINO_GROUND_Y + 1 {
                    print!("=");
                } else if x == DINO_X && y >= self.dino_y && y < self.dino_y + 2 {
                    if y == self.dino_y {
                        print!("D");
                    } else {
                        print!("T");
                    }
                } else if x >= self.obstacle_x
                    && x < self.obstacle_x + 2
                    && y >= DINO_GROUND_Y.saturating_sub(2)
                    && y <= DINO_GROUND_Y
                {
                    print!("|");
                } else {
                    print!(" ");
                }
            }
            println!();
        }

        io::stdout()
            .flush()
            .map_err(|err| format!("failed to flush stdout: {err}"))
    }

    fn run(&mut self, input: &mut DinoInput) -> Result<(), String> {
        let tick_duration = Duration::from_millis(DINO_TICK_MILLIS);

        println!("\nGame started!");
        println!("Press SPACE to jump over obstacles");
        println!("Press 'q' to quit");
        println!();

        self.render()?;

        loop {
            let frame_start = Instant::now();
            let mut quit_requested = false;

            loop {
                match input.poll() {
                    Some(b' ') => self.jump(),
                    Some(b'q') | Some(b'Q') => {
                        quit_requested = true;
                        break;
                    }
                    Some(b'\r') | Some(b'\n') => {}
                    Some(_) => {}
                    None => break,
                }
            }

            if quit_requested {
                println!("\nQuitting game...");
                println!("Final Score: {}", self.score);
                io::stdout()
                    .flush()
                    .map_err(|err| format!("failed to flush stdout: {err}"))?;
                return Ok(());
            }

            self.frame_count = self.frame_count.wrapping_add(1);
            if self.frame_count % DINO_GAME_SPEED == 0 {
                self.update();
            }

            if self.frame_count % DINO_FRAMES_PER_RENDER == 0 || self.game_over {
                self.render()?;
            }

            if self.game_over {
                println!("\nGame Over! Final Score: {}", self.score);
                println!("Press 'q' to return to shell...");
                io::stdout()
                    .flush()
                    .map_err(|err| format!("failed to flush stdout: {err}"))?;
                input.wait_for_quit();
                return Ok(());
            }

            let elapsed = frame_start.elapsed();
            if elapsed < tick_duration {
                thread::sleep(tick_duration - elapsed);
            }
        }
    }
}

fn soft_pwm_period(
    gpio: &GpioClient,
    timer: &TimerClient,
    channel: u32,
    duty_percent: u64,
) -> Result<(), String> {
    if duty_percent == 0 {
        gpio.set(GPIO3_BANK, GREEN_LED_PIN, gpio_proto::Level::Low)?;
        timer.wait_micros(channel, BREATH_PERIOD_US)?;
        return Ok(());
    }

    if duty_percent >= 100 {
        gpio.set(GPIO3_BANK, GREEN_LED_PIN, gpio_proto::Level::High)?;
        timer.wait_micros(channel, BREATH_PERIOD_US)?;
        return Ok(());
    }

    let high_time = (BREATH_PERIOD_US * duty_percent) / 100;
    let low_time = BREATH_PERIOD_US - high_time;

    if high_time > 0 {
        gpio.set(GPIO3_BANK, GREEN_LED_PIN, gpio_proto::Level::High)?;
        timer.wait_micros(channel, high_time)?;
    }

    if low_time > 0 {
        gpio.set(GPIO3_BANK, GREEN_LED_PIN, gpio_proto::Level::Low)?;
        timer.wait_micros(channel, low_time)?;
    }

    Ok(())
}

fn to_proto_level(level: gpio_cmd::Level) -> gpio_proto::Level {
    match level {
        gpio_cmd::Level::Low => gpio_proto::Level::Low,
        gpio_cmd::Level::High => gpio_proto::Level::High,
    }
}

fn display_proto_level(level: gpio_proto::Level) -> &'static str {
    match level {
        gpio_proto::Level::Low => "LOW",
        gpio_proto::Level::High => "HIGH",
    }
}

fn print_usage() {
    println!("sbox commands:");
    println!("  gpio <subcommand> ...  - GPIO control helpers (see 'sbox gpio --help')");
    println!("  reboot                 - Trigger CRU first-level reboot sequence");
    println!("  led_blink              - Blink the blue status LED using PMU timer");
    println!("  led_breath             - Software PWM breathing effect on green LED");
    println!("  dino                   - Play the Chrome-style Dino runner mini-game");
    println!("  detect_button          - Poll GPIO1_A4 and report a falling edge");
    println!("  help                   - Show this message");
}

fn print_gpio_usage() {
    for line in gpio_cmd::USAGE_LINES {
        println!("{line}");
    }
}

fn print_reboot_usage() {
    println!("Usage: sbox reboot");
    println!("Triggers RK3588 CRU first-level reboot after a short countdown.");
}

fn print_led_blink_usage() {
    println!("Usage: sbox led_blink");
    println!("Blinks the blue LED three times with one second on/off intervals.");
}

fn print_led_breath_usage() {
    println!("Usage: sbox led_breath");
    println!("Runs a software PWM breathing effect on the green LED.");
}

fn print_detect_button_usage() {
    println!("Usage: sbox detect_button");
    println!("Polls GPIO1_A4 (header pin 11) and reports when it transitions from high to low.");
    println!("Connect a momentary switch between pin 11 (GPIO1_A4) and pin 9 (GND).");
}

fn print_dino_usage() {
    println!("Usage: sbox dino");
    println!("Launches a simple ASCII endless runner. Press SPACE to jump, 'q' to quit.");
}

fn countdown_with_warning() {
    println!();
    println!("WARNING: system reboot will be triggered shortly!");
    for number in (1..=3).rev() {
        println!("{number}...");
        spin_delay(COUNTDOWN_SPIN_LOOPS);
    }
}

fn spin_delay(iterations: usize) {
    for _ in 0..iterations {
        core::hint::spin_loop();
    }
}
